package ilogrus

import (
	"fmt"
	log "github.com/sirupsen/logrus"
	"path"
	"runtime"
	"strings"
	"sync"
)

//logrus 代码日志文件，函数和代码行位置输出hook
//用法：
//lfh := NewLogLineNumHook()
//lfh.EnableFileLogLine = true
//lfh.EnableLogFuncName = true
//log.AddHook(lfh)
type LineNumLogrusHook struct {
	//启用文件名称log
	EnableFileNameLog bool
	//启用函数名称log
	EnableFuncNameLog bool
	//启用短函数名称
	EnableShortFuncName bool
	//是否启用自定义包名获取方式
	EnableCustomPackageNameMode bool
}

func NewLineNumLogrusHook() *LineNumLogrusHook {
	return &LineNumLogrusHook{
		EnableFileNameLog:           true,
		EnableFuncNameLog:           true,
		EnableShortFuncName:         true,
		EnableCustomPackageNameMode: true,
	}
}

func (hooks LineNumLogrusHook) Levels() []log.Level {
	return log.AllLevels
}

func (hook *LineNumLogrusHook) Fire(entry *log.Entry) error {

	var (
		file, function string
		line           int
		frame          *runtime.Frame
	)
	if hook.EnableCustomPackageNameMode {
		//自定义逻辑
		frame = getCaller()
	} else {
		//使用原生的逻辑
		if entry.HasCaller() {
			frame = entry.Caller
		}
	}

	if frame != nil {
		line = frame.Line
		function = frame.Function
		if hook.EnableShortFuncName {
			_, function = path.Split(function)
		}
		dir, filename := path.Split(frame.File)
		f := path.Base(dir)
		file = fmt.Sprintf("%s/%s", f, filename)
	}

	if hook.EnableFileNameLog && hook.EnableFuncNameLog {
		entry.Message = fmt.Sprintf("[%s(%s:%d)] %s", function, file, line, entry.Message)
	}
	//router/route_table.go(43)
	if hook.EnableFileNameLog && !hook.EnableFuncNameLog {
		entry.Message = fmt.Sprintf("[%s(%d)] %s", file, line, entry.Message)
	}
	//microservice-gateway/v1/router.(*RouteTable).AddRoutePattern(43)
	if !hook.EnableFileNameLog && hook.EnableFuncNameLog {
		entry.Message = fmt.Sprintf("[%s(%d)] %s", function, line, entry.Message)
	}

	return nil
}

var (
	bufferPool *sync.Pool

	// qualified package name, cached at first use
	logrusPackage string

	// Positions in the call stack when tracing to report the calling method
	minimumCallerDepth int

	// Used for caller information initialisation
	callerInitOnce sync.Once
)

const (
	maximumCallerDepth int = 30
	knownLogrusFrames  int = 4
)

// getCaller retrieves the name of the first non-logrus calling function
func getCaller() *runtime.Frame {

	// cache this package's fully-qualified name
	callerInitOnce.Do(func() {
		//pcs := make([]uintptr, 2)
		//_ = runtime.Callers(0, pcs)
		//fmt.Println(runtime.FuncForPC(pcs[0]).Name())
		//fmt.Println(runtime.FuncForPC(pcs[1]).Name())
		//logrusPackage = getPackageName(runtime.FuncForPC(pcs[0]).Name())
		logrusPackage = "github.com/sirupsen/logrus"
		// now that we have the cache, we can skip a minimum count of known-logrus functions
		// XXX this is dubious, the number of frames may vary
		minimumCallerDepth = knownLogrusFrames
	})

	// Restrict the lookback frames to avoid runaway lookups
	pcs := make([]uintptr, maximumCallerDepth)
	depth := runtime.Callers(minimumCallerDepth, pcs)
	frames := runtime.CallersFrames(pcs[:depth])
	for f, again := frames.Next(); again; f, again = frames.Next() {
		pkg := getPackageName(f.Function)

		// If the caller isn't part of this package, we're done
		if pkg != logrusPackage {
			return &f
		}
	}

	// if we got here, we failed to find the caller's context
	return nil
}

// getPackageName reduces a fully qualified function name to the package name
// There really ought to be to be a better way...
func getPackageName(f string) string {
	for {
		lastPeriod := strings.LastIndex(f, ".")
		lastSlash := strings.LastIndex(f, "/")
		if lastPeriod > lastSlash {
			f = f[:lastPeriod]
		} else {
			break
		}
	}

	return f
}
